{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 基本配置"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torchvision\n",
    "\n",
    "print(torch.__version__)\n",
    "print(torch.version.cuda)\n",
    "print(torch.backends.cudnn.version())\n",
    "print(torch.cuda.get_device_name(0))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 可复现性\n",
    "在硬件设备（CPU、GPU）不同时，完全的可复现性无法保证，即使随机种子相同。但是，在同一个设备上，\n",
    "应该保证可复现性。具体做法是，在程序开始的时候固定torch的随机种子，同时也把numpy的随机种子固定\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.random.seed(0)\n",
    "torch.manual_seed(0)\n",
    "torch.cuda.manual_seed_all(0)\n",
    "\n",
    "torch.backends.cudnn.deterministic = True\n",
    "torch.backends.cudnn.benchmark = False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 显卡设置"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#如果只需要一张显卡\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "#如果需要指定多张显卡，比如0，1号显卡。\n",
    "import os\n",
    "os.environ['CUDA_VISIBLE_DEVICES'] = '0,1'\n",
    "\n",
    "#也可以在命令行运行代码时设置显卡：\n",
    "\n",
    "CUDA_VISIBLE_DEVICES=0,1 python train.py"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "## 清除显存\n",
    "torch.cuda.empty_cache()\n",
    "\n",
    "#也可以使用在命令行重置GPU的指令\n",
    "nvidia-smi --gpu-reset -i [gpu_id]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 张量(Tensor)处理\n",
    "\n",
    "#### 1.张量的数据类型\n",
    "PyTorch有9种CPU张量类型和9种GPU张量类型"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 张量基本信息"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tensor = torch.randn(3,4,5)\n",
    "print(tensor.type())  # 数据类型\n",
    "print(tensor.size())  # 张量的shape，是个元组\n",
    "print(tensor.dim())   # 维度的数量"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 命名张量\n",
    "\n",
    "张量命名是一个非常有用的方法，这样可以方便地使用维度的名字来做索引或其他操作，大大提高了可读性、易用性，防止出错。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "NCHW = [‘N’, ‘C’, ‘H’, ‘W’]\n",
    "images = torch.randn(32, 3, 56, 56, names=NCHW)\n",
    "images.sum('C')\n",
    "images.select('C', index=0)\n",
    "# 也可以这么设置\n",
    "tensor = torch.rand(3,4,1,2,names=('C', 'N', 'H', 'W'))\n",
    "# 使用align_to可以对维度方便地排序\n",
    "tensor = tensor.align_to('N', 'C', 'H', 'W')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 数据类型转换"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 设置默认类型，pytorch中的FloatTensor远远快于DoubleTensor\n",
    "torch.set_default_tensor_type(torch.FloatTensor)\n",
    "\n",
    "# 类型转换\n",
    "tensor = tensor.cuda()\n",
    "tensor = tensor.cpu()\n",
    "tensor = tensor.float()\n",
    "tensor = tensor.long()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### torch.Tensor与np.ndarray转换\n",
    "除了CharTensor，其他所有CPU上的张量都支持转换为numpy格式然后再转换回来。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ndarray = tensor.cpu().numpy()\n",
    "tensor = torch.from_numpy(ndarray).float()\n",
    "tensor = torch.from_numpy(ndarray.copy()).float() # If ndarray has negative stride."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Torch.tensor与PIL.Image转换"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# pytorch中的张量默认采用[N, C, H, W]的顺序，并且数据范围在[0,1]，需要进行转置和规范化\n",
    "# torch.Tensor -> PIL.Image\n",
    "image = PIL.Image.fromarray(torch.clamp(tensor*255, min=0, max=255).byte().permute(1,2,0).cpu().numpy())\n",
    "image = torchvision.transforms.functional.to_pil_image(tensor)  # Equivalently way\n",
    "\n",
    "# PIL.Image -> torch.Tensor\n",
    "path = r'./figure.jpg'\n",
    "tensor = torch.from_numpy(np.asarray(PIL.Image.open(path))).permute(2,0,1).float() / 255\n",
    "tensor = torchvision.transforms.functional.to_tensor(PIL.Image.open(path)) # Equivalently way"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### np.ndarray与PIL.Image的转换"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "image = PIL.Image.fromarray(ndarray.astype(np.uint8))\n",
    "\n",
    "ndarray = np.asarray(PIL.Image.open(path))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 从只包含一个元素的张量中提取值"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "value = torch.rand(1).item()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 张量形变"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 在将卷积层输入全连接层的情况下通常需要对张量做形变处理，\n",
    "# 相比torch.view，torch.reshape可以自动处理输入张量不连续的情况。\n",
    "tensor = torch.rand(2,3,4)\n",
    "shape = (6, 4)\n",
    "tensor = torch.reshape(tensor, shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 打乱顺序"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tensor = tensor[torch.randperm(tensor.size(0))]  # 打乱第一个维度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 水平翻转"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# pytorch不支持tensor[::-1]这样的负步长操作，水平翻转可以通过张量索引实现\n",
    "# 假设张量的维度为[N, D, H, W].\n",
    "tensor = tensor[:,:,:,torch.arange(tensor.size(3) - 1, -1, -1).long()]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 复制张量"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Operation                 |  New/Shared memory | Still in computation graph |\n",
    "tensor.clone()            # |        New         |          Yes               |\n",
    "tensor.detach()           # |      Shared        |          No                |\n",
    "tensor.detach.clone()()   # |        New         |          No                |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 张量拼接"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "注意torch.cat和torch.stack的区别在于torch.cat沿着给定的维度拼接，\n",
    "而torch.stack会新增一维。例如当参数是3个10x5的张量，torch.cat的结果是30x5的张量，\n",
    "而torch.stack的结果是3x10x5的张量。\n",
    "'''\n",
    "tensor = torch.cat(list_of_tensors, dim=0)\n",
    "tensor = torch.stack(list_of_tensors, dim=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 将整数标签转为one-hot编码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# pytorch的标记默认从0开始\n",
    "tensor = torch.tensor([0, 2, 1, 3])\n",
    "N = tensor.size(0)\n",
    "num_classes = 4\n",
    "one_hot = torch.zeros(N, num_classes).long()\n",
    "one_hot.scatter_(dim=1, index=torch.unsqueeze(tensor, dim=1), src=torch.ones(N, num_classes).long())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 得到非零元素"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.nonzero(tensor)               # index of non-zero elements\n",
    "torch.nonzero(tensor==0)            # index of zero elements\n",
    "torch.nonzero(tensor).size(0)       # number of non-zero elements\n",
    "torch.nonzero(tensor == 0).size(0)  # number of zero elements"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 判断两个张量相等"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.allclose(tensor1, tensor2)  # float tensor\n",
    "torch.equal(tensor1, tensor2)     # int tensor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 张量扩展"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Expand tensor of shape 64*512 to shape 64*512*7*7.\n",
    "tensor = torch.rand(64,512)\n",
    "torch.reshape(tensor, (64, 512, 1, 1)).expand(64, 512, 7, 7)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 矩阵乘法"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Matrix multiplcation: (m*n) * (n*p) * -> (m*p).\n",
    "result = torch.mm(tensor1, tensor2)\n",
    "\n",
    "# Batch matrix multiplication: (b*m*n) * (b*n*p) -> (b*m*p)\n",
    "result = torch.bmm(tensor1, tensor2)\n",
    "\n",
    "# Element-wise multiplication.\n",
    "result = tensor1 * tensor2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 计算两组数据之间的两两欧式距离\n",
    "利用broadcast机制"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dist = torch.sqrt(torch.sum((X1[:,None,:] - X2) ** 2, dim=2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 模型定义和操作"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 一个简单两层卷积网络的示例"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# convolutional neural network (2 convolutional layers)\n",
    "class ConvNet(nn.Module):\n",
    "    def __init__(self, num_classes=10):\n",
    "        super(ConvNet, self).__init__()\n",
    "        self.layer1 = nn.Sequential(\n",
    "            nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),\n",
    "            nn.BatchNorm2d(16),\n",
    "            nn.ReLU(),\n",
    "            nn.MaxPool2d(kernel_size=2, stride=2))\n",
    "        self.layer2 = nn.Sequential(\n",
    "            nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),\n",
    "            nn.BatchNorm2d(32),\n",
    "            nn.ReLU(),\n",
    "            nn.MaxPool2d(kernel_size=2, stride=2))\n",
    "        self.fc = nn.Linear(7*7*32, num_classes)\n",
    "\n",
    "    def forward(self, x):\n",
    "        out = self.layer1(x)\n",
    "        out = self.layer2(out)\n",
    "        out = out.reshape(out.size(0), -1)\n",
    "        out = self.fc(out)\n",
    "        return out\n",
    "\n",
    "model = ConvNet(num_classes).to(device)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 双线性汇合（bilinear pooling）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = torch.reshape(N, D, H * W)                        # Assume X has shape N*D*H*W\n",
    "X = torch.bmm(X, torch.transpose(X, 1, 2)) / (H * W)  # Bilinear pooling\n",
    "assert X.size() == (N, D, D)\n",
    "X = torch.reshape(X, (N, D * D))\n",
    "X = torch.sign(X) * torch.sqrt(torch.abs(X) + 1e-5)   # Signed-sqrt normalization\n",
    "X = torch.nn.functional.normalize(X)                  # L2 normalization"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 多卡同步 BN（Batch normalization）\n",
    "当使用 torch.nn.DataParallel 将代码运行在多张 GPU 卡上时，PyTorch 的 BN 层默认操作是各卡上数据独立地计算均值和标准差，同步 BN 使用所有卡上的数据一起计算 BN 层的均值和标准差，缓解了当批量大小（batch size）比较小时对均值和标准差估计不准的情况，是在目标检测等任务中一个有效的提升性能的技巧。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sync_bn = torch.nn.SyncBatchNorm(num_features, eps=1e-05, momentum=0.1, affine=True,\n",
    "                                 track_running_stats=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 将已有网络的所有BN层改为同步BN层"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def convertBNtoSyncBN(module, process_group=None):\n",
    "    '''Recursively replace all BN layers to SyncBN layer.\n",
    "\n",
    "    Args:\n",
    "        module[torch.nn.Module]. Network\n",
    "    '''\n",
    "    if isinstance(module, torch.nn.modules.batchnorm._BatchNorm):\n",
    "        sync_bn = torch.nn.SyncBatchNorm(module.num_features, module.eps, module.momentum,\n",
    "                                         module.affine, module.track_running_stats, process_group)\n",
    "        sync_bn.running_mean = module.running_mean\n",
    "        sync_bn.running_var = module.running_var\n",
    "        if module.affine:\n",
    "            sync_bn.weight = module.weight.clone().detach()\n",
    "            sync_bn.bias = module.bias.clone().detach()\n",
    "        return sync_bn\n",
    "    else:\n",
    "        for name, child_module in module.named_children():\n",
    "            setattr(module, name) = convert_syncbn_model(child_module, process_group=process_group))\n",
    "        return module"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 类似 BN 滑动平均\n",
    "如果要实现类似 BN 滑动平均的操作，在 forward 函数中要使用原地（inplace）操作给滑动平均赋值。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BN(torch.nn.Module)\n",
    "    def __init__(self):\n",
    "        ...\n",
    "        self.register_buffer('running_mean', torch.zeros(num_features))\n",
    "\n",
    "    def forward(self, X):\n",
    "        ...\n",
    "        self.running_mean += momentum * (current - self.running_mean)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 计算模型整体参数量"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_parameters = sum(torch.numel(parameter) for parameter in model.parameters())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 查看网络中的参数\n",
    "可以通过model.state_dict()或者model.named_parameters()函数查看现在的全部可训练参数（包括通过继承得到的父类中的参数）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "params = list(model.named_parameters())\n",
    "(name, param) = params[28]\n",
    "print(name)\n",
    "print(param.grad)\n",
    "print('-------------------------------------------------')\n",
    "(name2, param2) = params[29]\n",
    "print(name2)\n",
    "print(param2.grad)\n",
    "print('----------------------------------------------------')\n",
    "(name1, param1) = params[30]\n",
    "print(name1)\n",
    "print(param1.grad)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 模型可视化（使用pytorchviz）\n",
    "szagoruyko/pytorchvizgithub.com\n",
    "#### 类似 Keras 的 model.summary() 输出模型信息，使用pytorch-summary\n",
    "sksq96/pytorch-summarygithub.com\n",
    "#### 模型权重初始化\n",
    "注意 model.modules() 和 model.children() 的区别：model.modules() 会迭代地遍历模型的所有子层，而 model.children() 只会遍历模型下的一层。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Common practise for initialization.\n",
    "for layer in model.modules():\n",
    "    if isinstance(layer, torch.nn.Conv2d):\n",
    "        torch.nn.init.kaiming_normal_(layer.weight, mode='fan_out',\n",
    "                                      nonlinearity='relu')\n",
    "        if layer.bias is not None:\n",
    "            torch.nn.init.constant_(layer.bias, val=0.0)\n",
    "    elif isinstance(layer, torch.nn.BatchNorm2d):\n",
    "        torch.nn.init.constant_(layer.weight, val=1.0)\n",
    "        torch.nn.init.constant_(layer.bias, val=0.0)\n",
    "    elif isinstance(layer, torch.nn.Linear):\n",
    "        torch.nn.init.xavier_normal_(layer.weight)\n",
    "        if layer.bias is not None:\n",
    "            torch.nn.init.constant_(layer.bias, val=0.0)\n",
    "\n",
    "# Initialization with given tensor.\n",
    "layer.weight = torch.nn.Parameter(tensor)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 提取模型中的某一层\n",
    "modules()会返回模型中所有模块的迭代器，它能够访问到最内层，比如self.layer1.conv1这个模块，还有一个与它们相对应的是name_children()属性以及named_modules(),这两个不仅会返回模块的迭代器，还会返回网络层的名字。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 取模型中的前两层\n",
    "new_model = nn.Sequential(*list(model.children())[:2]\n",
    "# 如果希望提取出模型中的所有卷积层，可以像下面这样操作：\n",
    "for layer in model.named_modules():\n",
    "    if isinstance(layer[1],nn.Conv2d):\n",
    "         conv_model.add_module(layer[0],layer[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 部分层使用预训练模型\n",
    "注意如果保存的模型是 torch.nn.DataParallel，则当前的模型也需要是"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.load_state_dict(torch.load('model.pth'), strict=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 将在 GPU 保存的模型加载到 CPU"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.load_state_dict(torch.load('model.pth', map_location='cpu'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 导入另一个模型的相同部分到新的模型\n",
    "模型导入参数时，如果两个模型结构不一致，则直接导入参数会报错。用下面方法可以把另一个模型的相同的部分导入到新的模型中。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# model_new代表新的模型\n",
    "# model_saved代表其他模型，比如用torch.load导入的已保存的模型\n",
    "model_new_dict = model_new.state_dict()\n",
    "model_common_dict = {k:v for k, v in model_saved.items() if k in model_new_dict.keys()}\n",
    "model_new_dict.update(model_common_dict)\n",
    "model_new.load_state_dict(model_new_dict)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据处理"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 计算数据集的均值和标准差"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import cv2\n",
    "import numpy as np\n",
    "from torch.utils.data import Dataset\n",
    "from PIL import Image"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def compute_mean_and_std(dataset):\n",
    "    # 输入PyTorch的dataset，输出均值和标准差\n",
    "    mean_r = 0\n",
    "    mean_g = 0\n",
    "    mean_b = 0\n",
    "\n",
    "    for img, _ in dataset:\n",
    "        img = np.asarray(img) # change PIL Image to numpy array\n",
    "        mean_b += np.mean(img[:, :, 0])\n",
    "        mean_g += np.mean(img[:, :, 1])\n",
    "        mean_r += np.mean(img[:, :, 2])\n",
    "\n",
    "    mean_b /= len(dataset)\n",
    "    mean_g /= len(dataset)\n",
    "    mean_r /= len(dataset)\n",
    "\n",
    "    diff_r = 0\n",
    "    diff_g = 0\n",
    "    diff_b = 0\n",
    "\n",
    "    N = 0\n",
    "\n",
    "    for img, _ in dataset:\n",
    "        img = np.asarray(img)\n",
    "\n",
    "        diff_b += np.sum(np.power(img[:, :, 0] - mean_b, 2))\n",
    "        diff_g += np.sum(np.power(img[:, :, 1] - mean_g, 2))\n",
    "        diff_r += np.sum(np.power(img[:, :, 2] - mean_r, 2))\n",
    "\n",
    "        N += np.prod(img[:, :, 0].shape)\n",
    "\n",
    "    std_b = np.sqrt(diff_b / N)\n",
    "    std_g = np.sqrt(diff_g / N)\n",
    "    std_r = np.sqrt(diff_r / N)\n",
    "\n",
    "    mean = (mean_b.item() / 255.0, mean_g.item() / 255.0, mean_r.item() / 255.0)\n",
    "    std = (std_b.item() / 255.0, std_g.item() / 255.0, std_r.item() / 255.0)\n",
    "    return mean, std"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 得到视频数据基本信息"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import cv2\n",
    "video = cv2.VideoCapture(mp4_path)\n",
    "height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))\n",
    "width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))\n",
    "num_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))\n",
    "fps = int(video.get(cv2.CAP_PROP_FPS))\n",
    "video.release()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TSN 每段（segment）采样一帧视频"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "K = self._num_segments\n",
    "if is_train:\n",
    "    if num_frames > K:\n",
    "        # Random index for each segment.\n",
    "        frame_indices = torch.randint(\n",
    "            high=num_frames // K, size=(K,), dtype=torch.long)\n",
    "        frame_indices += num_frames // K * torch.arange(K)\n",
    "    else:\n",
    "        frame_indices = torch.randint(\n",
    "            high=num_frames, size=(K - num_frames,), dtype=torch.long)\n",
    "        frame_indices = torch.sort(torch.cat((\n",
    "            torch.arange(num_frames), frame_indices)))[0]\n",
    "else:\n",
    "    if num_frames > K:\n",
    "        # Middle index for each segment.\n",
    "        frame_indices = num_frames / K // 2\n",
    "        frame_indices += num_frames // K * torch.arange(K)\n",
    "    else:\n",
    "        frame_indices = torch.sort(torch.cat((\n",
    "            torch.arange(num_frames), torch.arange(K - num_frames))))[0]\n",
    "assert frame_indices.size() == (K,)\n",
    "return [frame_indices[i] for i in range(K)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 常用训练和验证数据预处理\n",
    "其中 ToTensor 操作会将 PIL.Image 或形状为 H×W×D，数值范围为 [0, 255] 的 np.ndarray 转换为形状为 D×H×W，数值范围为 [0.0, 1.0] 的 torch.Tensor。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_transform = torchvision.transforms.Compose([\n",
    "    torchvision.transforms.RandomResizedCrop(size=224,\n",
    "                                             scale=(0.08, 1.0)),\n",
    "    torchvision.transforms.RandomHorizontalFlip(),\n",
    "    torchvision.transforms.ToTensor(),\n",
    "    torchvision.transforms.Normalize(mean=(0.485, 0.456, 0.406),\n",
    "                                     std=(0.229, 0.224, 0.225)),\n",
    " ])\n",
    " val_transform = torchvision.transforms.Compose([\n",
    "    torchvision.transforms.Resize(256),\n",
    "    torchvision.transforms.CenterCrop(224),\n",
    "    torchvision.transforms.ToTensor(),\n",
    "    torchvision.transforms.Normalize(mean=(0.485, 0.456, 0.406),\n",
    "                                     std=(0.229, 0.224, 0.225)),\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 模型训练和测试"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 分类模型训练代码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Loss and optimizer\n",
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)\n",
    "\n",
    "# Train the model\n",
    "total_step = len(train_loader)\n",
    "for epoch in range(num_epochs):\n",
    "    for i ,(images, labels) in enumerate(train_loader):\n",
    "        images = images.to(device)\n",
    "        labels = labels.to(device)\n",
    "\n",
    "        # Forward pass\n",
    "        outputs = model(images)\n",
    "        loss = criterion(outputs, labels)\n",
    "\n",
    "        # Backward and optimizer\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "\n",
    "        if (i+1) % 100 == 0:\n",
    "            print('Epoch: [{}/{}], Step: [{}/{}], Loss: {}'\n",
    "                  .format(epoch+1, num_epochs, i+1, total_step, loss.item()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 分类模型测试代码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test the model\n",
    "model.eval()  # eval mode(batch norm uses moving mean/variance\n",
    "              #instead of mini-batch mean/variance)\n",
    "with torch.no_grad():\n",
    "    correct = 0\n",
    "    total = 0\n",
    "    for images, labels in test_loader:\n",
    "        images = images.to(device)\n",
    "        labels = labels.to(device)\n",
    "        outputs = model(images)\n",
    "        _, predicted = torch.max(outputs.data, 1)\n",
    "        total += labels.size(0)\n",
    "        correct += (predicted == labels).sum().item()\n",
    "\n",
    "    print('Test accuracy of the model on the 10000 test images: {} %'\n",
    "          .format(100 * correct / total))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 自定义loss\n",
    "继承torch.nn.Module类写自己的loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyLoss(torch.nn.Moudle):\n",
    "    def __init__(self):\n",
    "        super(MyLoss, self).__init__()\n",
    "\n",
    "    def forward(self, x, y):\n",
    "        loss = torch.mean((x - y) ** 2)\n",
    "        return loss"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 标签平滑（label smoothing）\n",
    "写一个label_smoothing.py的文件，然后在训练代码里引用，用LSR代替交叉熵损失即可。label_smoothing.py内容如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "\n",
    "\n",
    "class LSR(nn.Module):\n",
    "\n",
    "    def __init__(self, e=0.1, reduction='mean'):\n",
    "        super().__init__()\n",
    "\n",
    "        self.log_softmax = nn.LogSoftmax(dim=1)\n",
    "        self.e = e\n",
    "        self.reduction = reduction\n",
    "\n",
    "    def _one_hot(self, labels, classes, value=1):\n",
    "        \"\"\"\n",
    "            Convert labels to one hot vectors\n",
    "\n",
    "        Args:\n",
    "            labels: torch tensor in format [label1, label2, label3, ...]\n",
    "            classes: int, number of classes\n",
    "            value: label value in one hot vector, default to 1\n",
    "\n",
    "        Returns:\n",
    "            return one hot format labels in shape [batchsize, classes]\n",
    "        \"\"\"\n",
    "\n",
    "        one_hot = torch.zeros(labels.size(0), classes)\n",
    "\n",
    "        #labels and value_added  size must match\n",
    "        labels = labels.view(labels.size(0), -1)\n",
    "        value_added = torch.Tensor(labels.size(0), 1).fill_(value)\n",
    "\n",
    "        value_added = value_added.to(labels.device)\n",
    "        one_hot = one_hot.to(labels.device)\n",
    "\n",
    "        one_hot.scatter_add_(1, labels, value_added)\n",
    "\n",
    "        return one_hot\n",
    "\n",
    "    def _smooth_label(self, target, length, smooth_factor):\n",
    "        \"\"\"convert targets to one-hot format, and smooth\n",
    "        them.\n",
    "        Args:\n",
    "            target: target in form with [label1, label2, label_batchsize]\n",
    "            length: length of one-hot format(number of classes)\n",
    "            smooth_factor: smooth factor for label smooth\n",
    "\n",
    "        Returns:\n",
    "            smoothed labels in one hot format\n",
    "        \"\"\"\n",
    "        one_hot = self._one_hot(target, length, value=1 - smooth_factor)\n",
    "        one_hot += smooth_factor / (length - 1)\n",
    "\n",
    "        return one_hot.to(target.device)\n",
    "\n",
    "    def forward(self, x, target):\n",
    "\n",
    "        if x.size(0) != target.size(0):\n",
    "            raise ValueError('Expected input batchsize ({}) to match target batch_size({})'\n",
    "                    .format(x.size(0), target.size(0)))\n",
    "\n",
    "        if x.dim() < 2:\n",
    "            raise ValueError('Expected input tensor to have least 2 dimensions(got {})'\n",
    "                    .format(x.size(0)))\n",
    "\n",
    "        if x.dim() != 2:\n",
    "            raise ValueError('Only 2 dimension tensor are implemented, (got {})'\n",
    "                    .format(x.size()))\n",
    "\n",
    "\n",
    "        smoothed_target = self._smooth_label(target, x.size(1), self.e)\n",
    "        x = self.log_softmax(x)\n",
    "        loss = torch.sum(- x * smoothed_target, dim=1)\n",
    "\n",
    "        if self.reduction == 'none':\n",
    "            return loss\n",
    "\n",
    "        elif self.reduction == 'sum':\n",
    "            return torch.sum(loss)\n",
    "\n",
    "        elif self.reduction == 'mean':\n",
    "            return torch.mean(loss)\n",
    "\n",
    "        else:\n",
    "            raise ValueError('unrecognized option, expect reduction to be one of none, mean, sum')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "或者直接在训练文件里做label smoothing"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for images, labels in train_loader:\n",
    "    images, labels = images.cuda(), labels.cuda()\n",
    "    N = labels.size(0)\n",
    "    # C is the number of classes.\n",
    "    smoothed_labels = torch.full(size=(N, C), fill_value=0.1 / (C - 1)).cuda()\n",
    "    smoothed_labels.scatter_(dim=1, index=torch.unsqueeze(labels, dim=1), value=0.9)\n",
    "\n",
    "    score = model(images)\n",
    "    log_prob = torch.nn.functional.log_softmax(score, dim=1)\n",
    "    loss = -torch.sum(log_prob * smoothed_labels) / N\n",
    "    optimizer.zero_grad()\n",
    "    loss.backward()\n",
    "    optimizer.step()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Mixup训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "beta_distribution = torch.distributions.beta.Beta(alpha, alpha)\n",
    "for images, labels in train_loader:\n",
    "    images, labels = images.cuda(), labels.cuda()\n",
    "\n",
    "    # Mixup images and labels.\n",
    "    lambda_ = beta_distribution.sample([]).item()\n",
    "    index = torch.randperm(images.size(0)).cuda()\n",
    "    mixed_images = lambda_ * images + (1 - lambda_) * images[index, :]\n",
    "    label_a, label_b = labels, labels[index]\n",
    "\n",
    "    # Mixup loss.\n",
    "    scores = model(mixed_images)\n",
    "    loss = (lambda_ * loss_function(scores, label_a)\n",
    "            + (1 - lambda_) * loss_function(scores, label_b))\n",
    "    optimizer.zero_grad()\n",
    "    loss.backward()\n",
    "    optimizer.step()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### L1 正则化"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "l1_regularization = torch.nn.L1Loss(reduction='sum')\n",
    "loss = ...  # Standard cross-entropy loss\n",
    "for param in model.parameters():\n",
    "    loss += torch.sum(torch.abs(param))\n",
    "loss.backward()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 不对偏置项进行权重衰减（weight decay）\n",
    "pytorch里的weight decay相当于l2正则"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bias_list = (param for name, param in model.named_parameters() if name[-4:] == 'bias')\n",
    "others_list = (param for name, param in model.named_parameters() if name[-4:] != 'bias')\n",
    "parameters = [{'parameters': bias_list, 'weight_decay': 0},\n",
    "              {'parameters': others_list}]\n",
    "optimizer = torch.optim.SGD(parameters, lr=1e-2, momentum=0.9, weight_decay=1e-4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 梯度裁剪（gradient clipping）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 得到当前学习率"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# If there is one global learning rate (which is the common case).\n",
    "lr = next(iter(optimizer.param_groups))['lr']\n",
    "\n",
    "# If there are multiple learning rates for different layers.\n",
    "all_lr = []\n",
    "for param_group in optimizer.param_groups:\n",
    "    all_lr.append(param_group['lr'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "另一种方法，在一个batch训练代码里，当前的lr是optimizer.param_groups[0]['lr']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 学习率衰减"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Reduce learning rate when validation accuarcy plateau.\n",
    "scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', patience=5, verbose=True)\n",
    "for t in range(0, 80):\n",
    "    train(...)\n",
    "    val(...)\n",
    "    scheduler.step(val_acc)\n",
    "\n",
    "# Cosine annealing learning rate.\n",
    "scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=80)\n",
    "# Reduce learning rate by 10 at given epochs.\n",
    "scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[50, 70], gamma=0.1)\n",
    "for t in range(0, 80):\n",
    "    scheduler.step()\n",
    "    train(...)\n",
    "    val(...)\n",
    "\n",
    "# Learning rate warmup by 10 epochs.\n",
    "scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda t: t / 10)\n",
    "for t in range(0, 10):\n",
    "    scheduler.step()\n",
    "    train(...)\n",
    "    val(...)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 优化器链式更新\n",
    "从1.4版本开始，torch.optim.lr_scheduler 支持链式更新（chaining），即用户可以定义两个 schedulers，并交替在训练中使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torch.optim import SGD\n",
    "from torch.optim.lr_scheduler import ExponentialLR, StepLR\n",
    "model = [torch.nn.Parameter(torch.randn(2, 2, requires_grad=True))]\n",
    "optimizer = SGD(model, 0.1)\n",
    "scheduler1 = ExponentialLR(optimizer, gamma=0.9)\n",
    "scheduler2 = StepLR(optimizer, step_size=3, gamma=0.1)\n",
    "for epoch in range(4):\n",
    "    print(epoch, scheduler2.get_last_lr()[0])\n",
    "    optimizer.step()\n",
    "    scheduler1.step()\n",
    "    scheduler2.step()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 模型训练可视化\n",
    "PyTorch可以使用tensorboard来可视化训练过程。安装和运行TensorBoard。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pip install tensorboard\n",
    "tensorboard --logdir=runs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用SummaryWriter类来收集和可视化相应的数据，放了方便查看，可以使用不同的文件夹，比如'Loss/train'和'Loss/test'。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "import numpy as np\n",
    "\n",
    "writer = SummaryWriter()\n",
    "\n",
    "for n_iter in range(100):\n",
    "    writer.add_scalar('Loss/train', np.random.random(), n_iter)\n",
    "    writer.add_scalar('Loss/test', np.random.random(), n_iter)\n",
    "    writer.add_scalar('Accuracy/train', np.random.random(), n_iter)\n",
    "    writer.add_scalar('Accuracy/test', np.random.random(), n_iter)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 保存与加载断点\n",
    "注意为了能够恢复训练，我们需要同时保存模型和优化器的状态，以及当前的训练轮数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "start_epoch = 0\n",
    "# Load checkpoint.\n",
    "if resume: # resume为参数，第一次训练时设为0，中断再训练时设为1\n",
    "    model_path = os.path.join('model', 'best_checkpoint.pth.tar')\n",
    "    assert os.path.isfile(model_path)\n",
    "    checkpoint = torch.load(model_path)\n",
    "    best_acc = checkpoint['best_acc']\n",
    "    start_epoch = checkpoint['epoch']\n",
    "    model.load_state_dict(checkpoint['model'])\n",
    "    optimizer.load_state_dict(checkpoint['optimizer'])\n",
    "    print('Load checkpoint at epoch {}.'.format(start_epoch))\n",
    "    print('Best accuracy so far {}.'.format(best_acc))\n",
    "\n",
    "# Train the model\n",
    "for epoch in range(start_epoch, num_epochs):\n",
    "    ...\n",
    "\n",
    "    # Test the model\n",
    "    ...\n",
    "\n",
    "    # save checkpoint\n",
    "    is_best = current_acc > best_acc\n",
    "    best_acc = max(current_acc, best_acc)\n",
    "    checkpoint = {\n",
    "        'best_acc': best_acc,\n",
    "        'epoch': epoch + 1,\n",
    "        'model': model.state_dict(),\n",
    "        'optimizer': optimizer.state_dict(),\n",
    "    }\n",
    "    model_path = os.path.join('model', 'checkpoint.pth.tar')\n",
    "    best_model_path = os.path.join('model', 'best_checkpoint.pth.tar')\n",
    "    torch.save(checkpoint, model_path)\n",
    "    if is_best:\n",
    "        shutil.copy(model_path, best_model_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 提取 ImageNet 预训练模型某层的卷积特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# VGG-16 relu5-3 feature.\n",
    "model = torchvision.models.vgg16(pretrained=True).features[:-1]\n",
    "# VGG-16 pool5 feature.\n",
    "model = torchvision.models.vgg16(pretrained=True).features\n",
    "# VGG-16 fc7 feature.\n",
    "model = torchvision.models.vgg16(pretrained=True)\n",
    "model.classifier = torch.nn.Sequential(*list(model.classifier.children())[:-3])\n",
    "# ResNet GAP feature.\n",
    "model = torchvision.models.resnet18(pretrained=True)\n",
    "model = torch.nn.Sequential(collections.OrderedDict(\n",
    "    list(model.named_children())[:-1]))\n",
    "\n",
    "with torch.no_grad():\n",
    "    model.eval()\n",
    "    conv_representation = model(image)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 提取 ImageNet 预训练模型多层的卷积特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class FeatureExtractor(torch.nn.Module):\n",
    "    \"\"\"Helper class to extract several convolution features from the given\n",
    "    pre-trained model.\n",
    "\n",
    "    Attributes:\n",
    "        _model, torch.nn.Module.\n",
    "        _layers_to_extract, list<str> or set<str>\n",
    "\n",
    "    Example:\n",
    "        >>> model = torchvision.models.resnet152(pretrained=True)\n",
    "        >>> model = torch.nn.Sequential(collections.OrderedDict(\n",
    "                list(model.named_children())[:-1]))\n",
    "        >>> conv_representation = FeatureExtractor(\n",
    "                pretrained_model=model,\n",
    "                layers_to_extract={'layer1', 'layer2', 'layer3', 'layer4'})(image)\n",
    "    \"\"\"\n",
    "    def __init__(self, pretrained_model, layers_to_extract):\n",
    "        torch.nn.Module.__init__(self)\n",
    "        self._model = pretrained_model\n",
    "        self._model.eval()\n",
    "        self._layers_to_extract = set(layers_to_extract)\n",
    "\n",
    "    def forward(self, x):\n",
    "        with torch.no_grad():\n",
    "            conv_representation = []\n",
    "            for name, layer in self._model.named_children():\n",
    "                x = layer(x)\n",
    "                if name in self._layers_to_extract:\n",
    "                    conv_representation.append(x)\n",
    "            return conv_representation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 微调全连接层"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = torchvision.models.resnet18(pretrained=True)\n",
    "for param in model.parameters():\n",
    "    param.requires_grad = False\n",
    "model.fc = nn.Linear(512, 100)  # Replace the last fc layer\n",
    "optimizer = torch.optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9, weight_decay=1e-4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 以较大学习率微调全连接层，较小学习率微调卷积层"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = torchvision.models.resnet18(pretrained=True)\n",
    "finetuned_parameters = list(map(id, model.fc.parameters()))\n",
    "conv_parameters = (p for p in model.parameters() if id(p) not in finetuned_parameters)\n",
    "parameters = [{'params': conv_parameters, 'lr': 1e-3},\n",
    "              {'params': model.fc.parameters()}]\n",
    "optimizer = torch.optim.SGD(parameters, lr=1e-2, momentum=0.9, weight_decay=1e-4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 其他注意事项"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "不要使用太大的线性层。因为nn.Linear(m,n)使用的是的内存，线性层太大很容易超出现有显存。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "不要在太长的序列上使用RNN。因为RNN反向传播使用的是BPTT算法，其需要的内存和输入序列的长度呈线性关系。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "model(x) 前用 model.train() 和 model.eval() 切换网络状态。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "不需要计算梯度的代码块用 with torch.no_grad() 包含起来。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "model.eval() 和 torch.no_grad() 的区别在于，model.eval() 是将网络切换为测试状态，例如 BN 和dropout在训练和测试阶段使用不同的计算方法。torch.no_grad() 是关闭 PyTorch 张量的自动求导机制，以减少存储使用和加速计算，得到的结果无法进行 loss.backward()。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "model.zero_grad()会把整个模型的参数的梯度都归零, 而optimizer.zero_grad()只会把传入其中的参数的梯度归零."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "torch.nn.CrossEntropyLoss 的输入不需要经过 Softmax。torch.nn.CrossEntropyLoss等价于 torch.nn.functional.log_softmax + torch.nn.NLLLoss。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "loss.backward() 前用 optimizer.zero_grad() 清除累积梯度。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "torch.utils.data.DataLoader 中尽量设置 pin_memory=True，对特别小的数据集如 MNIST 设置 pin_memory=False 反而更快一些。num_workers 的设置需要在实验中找到最快的取值。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "用 del 及时删除不用的中间变量，节约 GPU 存储。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用 inplace 操作可节约 GPU 存储，如"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = torch.nn.functional.relu(x, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "减少 CPU 和 GPU 之间的数据传输。例如如果你想知道一个 epoch 中每个 mini-batch 的 loss 和准确率，先将它们累积在 GPU 中等一个 epoch 结束之后一起传输回 CPU 会比每个 mini-batch 都进行一次 GPU 到 CPU 的传输更快。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用半精度浮点数 half() 会有一定的速度提升，具体效率依赖于 GPU 型号。需要小心数值精度过低带来的稳定性问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "时常使用 assert tensor.size() == (N, D, H, W) 作为调试手段，确保张量维度和你设想中一致。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "除了标记 y 外，尽量少使用一维张量，使用 n*1 的二维张量代替，可以避免一些意想不到的一维张量计算结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "统计代码各部分耗时"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with torch.autograd.profiler.profile(enabled=True, use_cuda=False) as profile:    ...print(profile)# 或者在命令行运行python -m torch.utils.bottleneck main.py"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用TorchSnooper来调试PyTorch代码，程序在执行的时候，就会自动 print 出来每一行的执行结果的 tensor 的形状、数据类型、设备、是否需要梯度的信息。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# pip install torchsnooperimport torchsnooper# 对于函数，使用修饰器@torchsnooper.snoop()# 如果不是函数，使用 with 语句来激活 TorchSnooper，把训练的那个循环装进 with 语句中去。with torchsnooper.snoop():    原本的代码"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
